/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License specifically applicable to
 * this software at http://developer.yahoo.com/yslow/license.txt, or the general
 * form at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is: YSlow
 *
 * The Initial Developer of the Original Code is Yahoo! Inc.
 *
 * Copyright (C) 2007, Yahoo! Inc. All Rights Reserved.
 *
 * Contributor(s):
 * ***** END LICENSE BLOCK ***** */

/////////////////////////////////////////////////////////
//This is the main constructor for YSlowContext Class object.
////////////////////////////////////////////////////////

YSLOW.YSlowContext = function(panel){
	this.defaultview = "";
	this.PAGE = {
		totalSize:0,
		overallScore:0,
		bPeeled:false,
		statusbar:false,
		readyState:'',
		initializeNode:false
	};

	// Support different views for each button in the panel (Performance, Stats, Components).
	// This is a hash where the key is the button's id (eg, "ysPerfButton") and the value is a DIV element.
	this.panel = panel;
	this.curButtonId = "";
	this.buttonViews = {};

    // We'll create all the ButtonViews as DIVs within this DIV.
    this.initDiv();
}

//////////////////////////////////////////////////////////////////////////////////////////
//These are the functions which will be shared by all the instances of YSlow object.
//Keeping all these functions inside the prototype property makes it more memory friendly.
//////////////////////////////////////////////////////////////////////////////////////////

YSLOW.YSlowContext.prototype.peel = function(sView) {
		this.PAGE["bPeeled"] = true;
        var doc = FirebugContext.window.document;

        //Lets eliminate a blank page (about:blank) and all other about:* pages like about: and about:cache
        if ( !doc || !doc.location || doc.location.href.indexOf("about") == 0 || "undefined" == typeof(doc.location.hostname) ) {
                alert("Please enter a valid website address before running YSlow.");
		        FirebugContext.getPanel('yslow').restoreButtons();
                return;
        }

        this.defaultview = sView;

        if ( "undefined" == typeof(doc.yslow_launched) ) {  // only run YSlow once
                doc.yslow_launched = true;
                this.document = doc;  // TODO - change this to targetDoc or browserDoc

                var doctype = "";
                try {
                        doctype = ( "undefined" != typeof(doc.contentType) ? doc.contentType : doc.mimeType.toLowerCase() );
                }
                catch(err) {
                        doctype = "html";
                }

                var version = "$Revision: 1.47 $";
                version = version.substring(11, version.length-2);  // strip out just the version #, eg, "1.7"
                if ( -1 != doctype.indexOf("htm") ) {       // only kick this off for "text/html" content types
	                this.yslowInit();
    	            this.doComponents();
				}
				else {
                        eprint("YSlowContext.peel: Unsupported content type found: " + doctype);
                }
        }
        else {
                this.doView(sView);
        }
}


YSLOW.YSlowContext.prototype.updateStatusBar = function() {
        if ( ! this.PAGE["statusbar"] ) {
        // only set the bar once
                this.PAGE["statusbar"] = true;

        // If some of the info isn't available, we have to run some code.
        if ( ! this.PAGE["overallScore"] ) {
            this.genPerformance();
        }
        if ( ! this.PAGE["totalSize"] ) {
            this.genStats();
        }

        var size = YSLOW.Utils.formatFloat( ( this.PAGE["totalSize"] ? this.PAGE["totalSize"]/1000 : 0 ), 1, "K");
        var score = this.PAGE["overallScore"];
            var grade = YSLOW.Utils.prettyScore(score, false, true);

        YSLOW.Utils.setStatusBar(grade, "yslowStatusGrade");
        YSLOW.Utils.setStatusBar(size, "yslowStatusSize");

		// Send a beacon.
		if ( YSLOW.getPref("yslow.optinBeacon", false) ) {
			this.sendBeacon(score, size);
 		}
 	}
}


YSLOW.YSlowContext.prototype.sendBeacon = function(score, size) {
        var spaceid = this.getPageSpaceid();
        var thisUrl = this.document.location.href;
        yslowBeaconImg = new Image();
        yslowBeaconImg.src = YSLOW.getPref("yslow.beaconUrl", "http://rtblab.pclick.yahoo.com/images/ysb.gif") +
"?w=" + parseInt(size) +
                "&o=" + parseInt(score) + "&s=" + spaceid + "&u=" + escape(thisUrl);
}

// Try to find a spaceid in the HTML document source.
YSLOW.YSlowContext.prototype.getPageSpaceid = function() {
        var aComponents = this.getComponents("doc");
        if ( aComponents[0] && aComponents[0]['responseText'] ) {
                var sHtml = aComponents[0].responseText;  // assume the first "doc" is the original HTML doc
                var aDelims =      ["%2fE%3d", "/S=", "SpaceID=", "?f="];   // the beginning delimiter
                var aTerminators = ["%2fR%3d", ":",   " ",        "&"];     // the terminating delimiter
                // Client-side counting (yzq) puts the spaceid in it as "/E=95810469/R=" but it's escaped!
                for ( var i = 0; i < aDelims.length; i++ ) {
                        var sDelim = aDelims[i];
                        if ( -1 != sHtml.indexOf(sDelim) ) {                 // if the delimiter is present
                                var i1 = sHtml.indexOf(sDelim) + sDelim.length;  // skip over the delimiter
                                var i2 = sHtml.indexOf(aTerminators[i], i1);     // find the terminator
                                if ( -1 != i2 && (i2 - i1) < 15 ) {              // if the spaceid is < 15 digits
                                        var spaceid = sHtml.substring(i1, i2);
                                        if ( spaceid == parseInt(spaceid) ) {        // make sure it's all digits
                                                return spaceid;
                                        }
                                }
                        }
                }
        }

        return "";
}


////////////////////////////////////////////////////////////////////////////////
// UTILS END
////////////////////////////////////////////////////////////////////////////////

// Open a div to show the progress bars while finding the components in the page.
YSLOW.YSlowContext.prototype.doComponents = function() {
	if ( this.yslow_progbar ) {
		YSLOW.Utils.createProgressBar();
	}

	//Creating a peeler object.
	if ( typeof this.peelerObj != "undefined") {
		ddd("ERROR: YSLOW.YSlowContext.prototype.doComponents: Peeler object already exists.");
	}

	this.peelerObj = new YSLOW.Peeler(this);
	this.peelerObj.findComponents();

	// The code below findComponents is asynchronous, so it will continue the thread
	// to call doneFinding(), fetchComponentDetails(), and finally doneFetching().
}





// Initialize expected arrays and hashes.
// Mostly used to reduce the number of global variables.
YSLOW.YSlowContext.prototype.yslowInit = function() {
	this.yslow_progbar = true;
	var version = "$Revision: 1.47 $";
	this["yslow.user.js.version"] = version.substring(11, version.length-2);  // strip out just the version #, eg, "1.7"
	this.outerbarName = "outerbar";
	this.progbarName = "progbar";
	this.comps = [];
	this.columns_headers = ["Type", "URL", "Expires", "Gzip", "RespTime", "Size<br>(Ungzip)", "ETag"];
    this.columns_sort = ["type", "url", "expires", "compressed", "responsetime", "size", "etag"];
	this.count = {};
	this.lintweights = { "NumComps"	: 8,
						  "CDN"		: 6,
						  "Expires"		: 10,
						  "Gzip"		: 8,
						  "CssAtTop"	: 4,
						  "JsAtBottom"	: 4,
						  "Expression"	: 3,
						  "ExternalFiles"	: 4,
						  "Domains"		: 3,
						  "Obfuscate"	: 4,
						  "Redirects"	: 4,
						  "JsTwice"		: 4,
						  "ETags"		: 2
						};

	// Get the base hostname for components that have relative paths.
	var basehostname;     // used for components with a relative path, eg, /images/foo.gif
	var basehref;         // used for components with NO path, eg, foo.gif
	var aElements = this.document.getElementsByTagName("base");
	if ( "undefined" != typeof(aElements) && aElements && 0 < aElements.length ) {
		// Get it from the BASE tag instead.
		basehostname = aElements[0].href;
		basehref = aElements[0].href;
	}
	else {
		basehostname = this.document.location.protocol + "//" + this.document.location.host;
        basehref = basehostname + this.document.location.pathname;
	}
	// Make sure the base hostname is ONLY a hostname (no path) and does NOT end in slash.
	var iDot = basehostname.indexOf(".");    // the hostname should contain a dot (".")
	if ( -1 != iDot ) {
		// Cut off anything after the hostname.
		var iSlash = basehostname.indexOf("/", iDot);
		if ( -1 != iSlash ) {
			basehostname = basehostname.substring(0, iSlash);
		}
	}
	// Make sure the base href preserves the path up to the last slash and ends in slash.
	var iSlash = basehref.lastIndexOf("/");    // find the last "/"
	if ( -1 == iSlash ) {
		basehref += "/";
	}
	else {
		// Cut off anything after the last slash.
		basehref = basehref.substring(0, iSlash+1);
	}
	this.basehostname = basehostname;
	this.basehref = basehref;
}


YSLOW.YSlowContext.prototype.doView = function(sView) {
	sView = sView || this.defaultview;

	// Attach style sheet referencing the css in the chrome folder
	FirebugContext.getPanel("yslow").panelNode.id = "yslowDiv";
	var stext = "";

	if ( this.getButtonView(sView) ) {
		// This view already exists, just toggle to it.
		this.showButtonView(sView);
	}
	else if ( "ysCompsButton" == sView ) {
		stext += this.genComponents();
		this.addButtonView("ysCompsButton", stext);
	}
	else if ( "ysStatsButton" == sView ) {
		stext += this.genStats();
		this.addButtonView("ysStatsButton", stext);
	}
	else {
		// Default is Performance.
		stext += this.genPerformance();
		this.addButtonView("ysPerfButton", stext);
	}

    FirebugContext.getPanel("yslow").panelNode.scrollTop = 0;
    FirebugContext.getPanel("yslow").panelNode.scrollLeft = 0;

    this.updateStatusBar();
}


YSLOW.YSlowContext.prototype.viewHeaders = function(sId) {
	var headersDiv = this.getViewElementById(sId);
	if ( headersDiv ) {
		if ( "none" == headersDiv.style.display ) {
			headersDiv.style.display = "block";
		}
		else {
			headersDiv.style.display = "none";
		}
	}
}




// Generic functions to support expand/collapse divs.
// The only requirements are:
//		- you MUST use the "exp_col_div" class
//		- the expand/collapse image must have the same id as the div with "_img" appended at the end
YSLOW.YSlowContext.prototype.exp_col = function(id, bExpand) {
	var element = this.getViewElementById(id);
	var image = this.getViewElementById(id + "_img");
	if ( element && image ) {
		if ( "undefined" == typeof(bExpand) ) {
			bExpand = ( -1 == image.src.indexOf("open") );
		}
		element.style.display = ( bExpand ? "block" : "none" );
		image.src = "http://us.i1.yimg.com/us.yimg.com/i/us/plus/csp/cn/norgie_" + ( bExpand ? "open" : "closed" ) + "_dna.gif";
	}
}


YSLOW.YSlowContext.prototype.exp_col_all = function(bExpand, parentId) {
	var parent = null;
	if ( parentId ) {
		parent = this.getViewElementById(parentId);
	}

	if ( parent ) {
	    var aElements = YSLOW.Utils.getElementsByTagNameAndClass("div", "exp_col_div", parent);
		for (var i = 0; i < aElements.length; i++) {
			var id = aElements[i].id;
			this.exp_col(id, bExpand);
		}
	}
}


// Display the performance info for a page.
YSLOW.YSlowContext.prototype.genPerformance = function() {
    //dprint(9, "genPerformance: enter function");
	this.lintscores = {};
	var hLints = [];   // array of hashes, one hash per lint rule results
	hLints[hLints.length] = YSLOW.Lint.lintNumComps();
	hLints[hLints.length] = YSLOW.Lint.lintCDN();
	hLints[hLints.length] = YSLOW.Lint.lintExpires();
	hLints[hLints.length] = YSLOW.Lint.lintGzip();
	hLints[hLints.length] = YSLOW.Lint.lintCssAtTop();
	hLints[hLints.length] = YSLOW.Lint.lintJsAtBottom();
	hLints[hLints.length] = YSLOW.Lint.lintExpression();
	hLints[hLints.length] = YSLOW.Lint.lintExternalFiles();
	hLints[hLints.length] = YSLOW.Lint.lintDomains();
	hLints[hLints.length] = YSLOW.Lint.lintMinify();
	hLints[hLints.length] = YSLOW.Lint.lintRedirects();
	hLints[hLints.length] = YSLOW.Lint.lintJsTwice();
	hLints[hLints.length] = YSLOW.Lint.lintEtags();

	var tScore = 0, numScores = 0;
	// TODO - change this to use hLints, add a lintscore key to hLints (eg, "numcomps")
	for ( var key in this.lintscores ) {
		var iScore = this.lintscores[key];
		if ( iScore < 0 )
			iScore = 0;
		var iWeight = this.lintweights[key];
		if ( "undefined" == typeof(iWeight) || iWeight < 0 || iWeight > 10 ) {
			iWeight = 5;
		}
		tScore += iScore*iWeight;
		numScores += iWeight;
	}

	var overallScore = ( 0 == numScores ? 0 : tScore/numScores );
	this.PAGE.overallScore = overallScore;

	var sText = "";
	// header line
	// Performance Grade
	sText += '<table style="padding: 0px;" width=100% border=0 cellpadding=2 cellspacing=0>' +
			 '<tr style="background-color: #BBB;"><td class=rule style="border-width: 0;" valign=middle><b>Performance Grade:&nbsp;' + YSLOW.Utils.prettyScore(overallScore, true) + '&nbsp;(' + parseInt(overallScore) + ')</b></td>' +
             '<td align=right>' + ( this.bPrintable ? "" : '<a class=action href="javascript:document.yslowContext.exp_col_all(true, \'perfDiv\')">Expand All</a>  <a class=action href="javascript:document.yslowContext.exp_col_all(false, \'perfDiv\')">Collapse All</a>' ) +
             '</td>' +
             '</tr>' +
			 '</table>';

	// Warn user these results are limited because security permissions have not been enabled.
	if ( this.bPermDisabled ) {
		sText += YSLOW.Lint.genSecurityWarning();
	}

	sText += '<div id="perfDiv"><table style="padding: 8px;" border=0 cellpadding=1 cellspacing=0 width=100%>\n';

	// one row for each rule
	for ( var i = 0; i < hLints.length; i++ ) {
		var hLint = hLints[i];
		if ( 0 > hLint["score"] ) {
			hLint["score"] = 0;
		}
		sText += '<tr><td class=reportcardleft align=center valign=top><a href="" style="text-decoration: none; cursor:help;" title="' + parseInt(hLint["score"]) + '" onclick="return false;">' + YSLOW.Utils.prettyScore(hLint["score"]) + '</td>' +
				'<td class=reportcardright><a class=rule href="'+ hLint["rulehref"] +'" onclick="document.yslowPanel.openLink(\'' + hLint["rulehref"] + '\'); return false;" >' + (i+1) + '. ' + hLint["ruletext"] + '</a>';
		var sMore = ( hLint["warnings"] ? "<div class=warning style='color: #D23620;'>" + hLint["warnings"] + "</div>" : "" ) +
					( hLint["nonwarnings"] ? "<div class=nonwarning style='color: #008;'>" + hLint["nonwarnings"] + "</div>" : "" );
		if ( sMore ) {
			var exp_col_id = "lintDiv_" + (i+1);
			sText += ( this.bPrintable ? '' : '<a href="javascript:document.yslowContext.exp_col(\'' + exp_col_id + '\')"><img id="' + exp_col_id + '_img" border=0 src="http://us.i1.yimg.com/us.yimg.com/i/us/plus/csp/cn/norgie_closed_dna.gif"></a>' );
			sText += '<div id="' + exp_col_id + '" class=exp_col_div' +
					 ( this.bPrintable ? ' style="display: block;"' : '' ) +
					 '>' + sMore + '</div>';
		}
		sText += '</td>' + '</tr>\n';
	}
	sText += '</table></div>\n';

	return sText;
}

YSLOW.YSlowContext.prototype.genComponents = function() {
    //dprint(9, "genComponents: enter function");

	var sText = "";

	// header line

	// Warn user these results are limited because security permissions have not been enabled.
	if ( this.bPermDisabled ) {
		sText += YSLOW.Lint.genSecurityWarning();
	}

 	// COMPONENTS
 	sText += '<table style="margin: 8px;" border=0 cellpadding=0 cellspacing=0><tr><td class=reportcard style="padding: 0px;">' + this.printComponents() + '</td></tr></table>';

	return sText;
}

//YSlow Object
//Each external component is given a unique ID. So this is fast way to retrieve a component.
YSLOW.YSlowContext.prototype.getComponent = function(id) {
	var aComponents = this.getComponents();
	for ( var i = 0; i < aComponents.length; i++ ) {
		if ( id == YSLOW.Component.getCompVal(aComponents[i], "id") ) {
			return aComponents[i];
		}
	}

	return null;
}


YSLOW.YSlowContext.prototype.genStats = function() {
    //dprint(9, "genStats: enter function");
	var sText = "";

	// Page summary.
 	sText += '<table style="margin: 8px;" border=0 cellpadding=0 cellspacing=0>' +
			 '<tr><td class="reportcard rule" style="background-color: #BBBBBB; border-bottom: 0px;" align=center><b>Empty Cache</b></td>' +
			     '<td class="reportcard rule" style="background-color: #BBBBBB; border-bottom: 0px; border-left: 0px;" align=center><b>Primed Cache</b></td></tr>' +
			 '<tr><td class=reportcard valign=bottom bgcolor=#FFFFFF>' + this.genPageStats() +
			 '</td><td class=reportcard style="border-left: 0px" valign=bottom bgcolor=#EFEFEF>' + this.genPageStats(true) + '</td></tr>' +
			 '</table>';

	// Cookies.
    // By default, have the Cookies displayed
 	sText += '<table style="padding-left: 8px; padding-top: 20px" border=0 cellpadding=0 cellspacing=0>' +
			 '<tr><td class="rule">' +
			 ( this.bPrintable ? '' : '<a href="javascript:document.yslowContext.exp_col(\'cookiesDiv\')"><img id="cookiesDiv_img" border=0 src="http://us.i1.yimg.com/us.yimg.com/i/us/plus/csp/cn/norgie_open_dna.gif"></a>' ) +'&nbsp;' +
			 'Cookies: ' + this.document.cookie.length + ' bytes' +
			 '<div id="cookiesDiv" class="exp_col_div" style="border: 1px solid #EFEFEF; padding: 0px; ' +
			  'display: block; ' + '">';

	// List the cookies one per line, alternating background colors.
	var aCookies = this.document.cookie.split("; ");
        sText += '<table style="margin-left: 8px;" border=0 cellpadding=0 cellspacing=0>';
	for ( var i = 0; i < aCookies.length; i++ ) {
	        var sColor = ( 0 == i % 2 ? ' bgcolor=#FFFFFF' : ' bgcolor=#EFEFEF' );
		sText += "<tr " + sColor + " ><td><nobr>(" + aCookies[i].length + " bytes) " + aCookies[i] + "; </nobr></td></tr>";

	}
        sText += '</table>';

	sText += '</div></td></tr></table>';

	return sText;
}


// Generate stats for the page in either empty cache or primed cache mode.
YSLOW.YSlowContext.prototype.genPageStats = function(bCacheFull) {
	// Quick exit (because this can be called multiple times.
	if ( bCacheFull && "string" == typeof(this.primedCacheStats) ) {
		return this.primedCacheStats;
	}
	else if ( !bCacheFull && "string" == typeof(this.emptyCacheStats) ) {
		return this.emptyCacheStats;
	}

 	// SUMMARY - Find the number and total size for the categories.
	// Iterate over all the components and add things up.
	var hCount = {}, hSize = {};   // hashes where the key is the object type
	var nHttpRequests = 0;
	for (var i = 0; i < this.comps.length; i++) {
		var compObj = this.comps[i];

		if ( bCacheFull && YSLOW.Component.hasFarFutureExpiresOrMaxAge(compObj) ) {
			// If the object has a far future Expires date it won't add any HTTP requests nor size to the page.
		}
		else {
			// It adds to the HTTP requests (at least a condition GET request).
			nHttpRequests++;

			var compType = YSLOW.Component.getCompVal(compObj, "type");
			hCount[compType] = ( "undefined" == typeof(hCount[compType]) ? 1 : hCount[compType]+1 ); // init if necessary

			var size = 0;
			if ( !bCacheFull || !YSLOW.Component.hasOldModifiedDate(compObj) ) {
				// If we're doing EMPTY cache stats OR this component is newly modified (so is probably changing).
				if ( YSLOW.Component.getCompVal(compObj, "content-length") ) {
					size = parseInt( YSLOW.Component.getCompVal(compObj, "content-length") );
				}
				else if ( YSLOW.Component.getCompVal(compObj, "ungzipped_size") ) { // it only has an ungzipped_size if it was gzipped
					size = parseInt( YSLOW.Component.getCompVal(compObj, "ungzipped_size") / 3 );
				}
			}
			hSize[compType] = ( "undefined" == typeof(hSize[compType]) ? size : hSize[compType]+size ); // init if necessary
		}
	}

	// Iterate over the component types and format the SUMMARY results.
	var sText = "";
 	sText += '<table border=0 cellpadding=0 cellspacing=0 style="margin: 8px;">';

	// One row for each component type.
	var totalSize = 0;
	var aTypes = this.peelerObj.types;
 	for (var i = 0; i < aTypes.length; i++) {
 		var sType = aTypes[i];
 		if ( "undefined" != typeof(hCount[sType]) ) {
			sText += '<tr><td class="tdspace1 rule" align=right>' + YSLOW.Utils.kbSize(hSize[sType]) + '</td><td class="tdspace1 rule" align=right>' + hCount[sType] +
					 '</td><td class="tdspace1 rule" align=left>' + YSLOW.Utils.prettyType(sType) + (1 == hCount[sType] ? '' : 's') + '</td></tr>\n';
			totalSize += hSize[sType];
		}
	}

	// The total.
	sText += '<tr><td align=right class="tdspace1 totalsize rule" valign=bottom>' + YSLOW.Utils.kbSize(totalSize) +
			 '</td><td class="tdspace1 rule" align=left colspan=2 valign=bottom><b>Total size</b></td></tr>\n';
	sText += '<tr><td align=right class="tdspace1 rule"><b>' + nHttpRequests +
			 '</b></td><td class="tdspace1 rule" align=left colspan=2><b>HTTP requests</b></td></tr>\n';
	sText += '</table>\n';

	if ( bCacheFull ) {
		this.primedCacheStats = sText;
	}
	else {
		this.PAGE.totalSize = totalSize;
		this.emptyCacheStats = sText;
	}

	return sText;
}

// Return an array of all the components.
// If sType is specified only return components of that type.
YSLOW.YSlowContext.prototype.getComponents = function(sType) {
    //dprint(9, "getComponents: enter function");
	if ( "string" != typeof(sType) ) {
		return this["comps"];
	}

	var comps = [];
	for (var i = 0; i < this["comps"].length; i++) {
		var compObj = this["comps"][i];
		var curType = YSLOW.Component.getCompVal(compObj, "type");
		if ( curType == sType ) {
			comps[comps.length] = compObj;
		}
	}

	return comps;
}


// The browser document may contain "subdocuments" (iframes, frames, etc.).
// Return the topmost "parent" document (ie, window.document).
YSLOW.YSlowContext.prototype.getParentDocComponent = function() {
    //dprint(9, "getParentDocComponent: enter function");
	var aComponents = this.getComponents("doc");
	var parentDoc = aComponents[0]; // use the first one by default (necessary for YSlowSolo?)

	// See if any of the doc components have the "bParent" flag set.
	for ( var i = 0; i < aComponents.length; i++ ) {
		if ( aComponents[i].bParent ) {
			parentDoc = aComponents[i];
			break;
		}
	}

	return parentDoc;
}


// Generate a string of output for all the components in the page.
YSLOW.YSlowContext.prototype.printComponents = function() {
    //dprint(9, "YSLOW.YSlowContext.prototype.printComponents: enter function");

	var sResults = '<table style="padding: 0px;" border=0 cellpadding=0 cellspacing=1>';

	// column headers
	sResults += ' <tr>';
	for ( var iColumn = 0; iColumn < this["columns_headers"].length; iColumn++ ) {
		var sColumn = this["columns_headers"][iColumn];
		var sClass = "reportcardmiddle";
		if ( 0 == iColumn )
			sClass = "reportcardleft";
		else if ( iColumn+1 == this["columns_headers"].length )
			sClass = "reportcardright";
        sResults += '    <td class="' + sClass + ' rule" style="background-color: #BBBBBB; border-top: 0px;" align="center"><b>' + sColumn + '</b></td>\n';
	}
	sResults += ' </tr>\n';

	// First, iterate through all the components and build a TR string for each one.
	for (var i = 0; i < this.comps.length; i++) {
		var compObj = this.comps[i];
		sResults += YSLOW.Component.printComponent(compObj, i);
	}

	sResults += '</table>\n';

	return sResults;
}

// sort and regenerate components
YSLOW.YSlowContext.prototype.regenComponents = function(sortBy) {

    var sortBy = sortBy || 'type';

    this.peelerObj.sortComponents(sortBy);
    this.setButtonView('ysCompsButton', this.genComponents());
    return;

}

////////////////////////////////////////////////////////////////////////////////
// BUTTON VIEWS
////////////////////////////////////////////////////////////////////////////////

YSLOW.YSlowContext.prototype.initDiv = function() {
	dprint(9, "initDiv: this.panel = " + this.panel);

    var elem = this.panel.document.createElement("div");
    elem.style.display = "block";
	if ( this.panel.panelNode ) { // avoid errors in YSlowSolo
	    this.panel.panelNode.appendChild(elem);
	}

	// If you want to add HTML to a YSlowContext, do that view its viewNode.
	// viewNode is to YSlowContext as panelNode is to YSlowPanel.
    this.viewNode = elem;
};


YSLOW.YSlowContext.prototype.show = function() {
    // Select the current button.
	dprint(9, "YSlowContext.show: curButtonId = " + this.curButtonId);
    if ( FirebugChrome.$(this.curButtonId) ) {
		// In some cases, we have a buttonView without a button (like "panel_about").
	    FirebugChrome.$(this.curButtonId).checked = true;
	}

    // Display the view for the currently selected button.
    this.showButtonView(this.curButtonId);
};


YSLOW.YSlowContext.prototype.addButtonView = function(sButtonId, sHtml) {
	dprint(9, "YSlowContext.addButtonView: enter: sButtonId = " + sButtonId);
    var btnView = this.getButtonView(sButtonId);
    if ( ! btnView ) {
        btnView = this.panel.document.createElement("div");
        btnView.style.display = "none";
        this.viewNode.appendChild(btnView);
        this.buttonViews[sButtonId] = btnView;
    }

    btnView.innerHTML = sHtml;
    this.showButtonView(sButtonId);
};


YSLOW.YSlowContext.prototype.setButtonView = function(sButtonId, sHtml) {
    var btnView = this.getButtonView(sButtonId);
    if ( ! btnView ) {
		eprint("ERROR: YSlowContext.setButtonView: Couldn't find ButtonView '" + sButtonId + "'.");
		return;
    }

    btnView.innerHTML = sHtml;
    this.showButtonView(sButtonId);
};


YSLOW.YSlowContext.prototype.getButtonView = function(sButtonId) {
    return ( this.buttonViews.hasOwnProperty(sButtonId) ? this.buttonViews[sButtonId] : undefined );
};


YSLOW.YSlowContext.prototype.showButtonView = function(sButtonId) {
    var btnView = this.getButtonView(sButtonId);

    if ( ! btnView ) {
        eprint("ERROR: YSLOW.YSlowContext.showButtonView: Couldn't find ButtonView named '" + sButtonId + "'.");
        return;
    }

    // Hide all the other button views.
    for ( var sId in this.buttonViews ) {
        if ( this.buttonViews.hasOwnProperty(sId) && sId != sButtonId ) {
            this.buttonViews[sId].style.display = "none";
        }
    }

    btnView.style.display = "block";
    this.curButtonId = sButtonId;
};



YSLOW.YSlowContext.prototype.getCurButtonView = function() {
    return this.getButtonView(this.curButtonId);
};


// If we want to find an element in the current buttonView,
// we limit our search to just the elements in it.
YSLOW.YSlowContext.prototype.getViewElementById = function(sId) {
	dprint(9, "YSlowContext.getViewElementById: enter: sId = " + sId);
    var aElements = this.getCurButtonView().getElementsByTagName("*");
    for ( var i = 0; i < aElements.length; i++ ) {
        var elem = aElements[i];
		dprint(9, "YSlowContext.getViewElementById: enter: elem.id = " + elem.id);
        if ( sId == elem.id ) {
            return elem;
        }
    }

    eprint("ERROR: getCurElement: Unable to find element '" + sId + "'.");
	return undefined;
};
